/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores
 * CA 94065 USA or visit www.oracle.com if you need additional information or
 * have any questions.
 */
package com.codename1.ui.util;

import com.codename1.io.Log;
import com.codename1.ui.CN;
import com.codename1.ui.Display;
import com.codename1.ui.EncodedImage;
import com.codename1.ui.Font;
import com.codename1.ui.Image;
import com.codename1.ui.animations.AnimationObject;
import com.codename1.ui.animations.Timeline;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.plaf.Border;
import com.codename1.ui.plaf.CSSBorder;
import com.codename1.ui.plaf.RoundBorder;
import com.codename1.ui.plaf.RoundRectBorder;
import com.codename1.ui.plaf.Style;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Loads resources from the binary resource file generated by the Codename One Designer.
 * A resource is loaded entirely into memory since random file access is not supported
 * in all platforms. Any other approach would be inefficient. This means that memory must
 * be made available to accommodate the resource file.
 *
 * @author Shai Almog
 */
public class Resources {

    /**
     * Magic numbers to prevent data corruption
     */
    static final byte MAGIC_THEME_LEGACY = (byte) 0xF7;
    static final byte MAGIC_ANIMATION_LEGACY = (byte) 0xF8;
    static final byte MAGIC_INDEXED_IMAGE_LEGACY = (byte) 0xF4;
    static final byte MAGIC_FONT_LEGACY = (byte) 0xF6;
    static final byte MAGIC_INDEXED_FONT_LEGACY = (byte) 0xFB;
    static final byte MAGIC_IMAGE_LEGACY = (byte) 0xF3;
    static final byte MAGIC_SVG = (byte) 0xF5;
    static final byte MAGIC_TIMELINE = (byte) 0xEF;
    static final byte MAGIC_FONT = (byte) 0xFC;
    static final byte MAGIC_IMAGE = (byte) 0xFD;
    static final byte MAGIC_L10N = (byte) 0xF9;
    static final byte MAGIC_DATA = (byte) 0xFA;
    static final byte MAGIC_UI = (byte) 0xEE;
    static final byte MAGIC_HEADER = (byte) 0xFF;
    static final byte MAGIC_PASSWORD = (byte) 0xFE;
    /**
     * Temporary member for compatibility with older versions, in future versions
     * this will supersede the MAGIC_THEME property
     */
    static final byte MAGIC_THEME = (byte) 0xF2;
    static final int BORDER_TYPE_EMPTY = 0;
    static final int BORDER_TYPE_LINE = 1;
    static final int BORDER_TYPE_ROUNDED = 2;
    static final int BORDER_TYPE_ETCHED_LOWERED = 4;
    static final int BORDER_TYPE_ETCHED_RAISED = 5;
    static final int BORDER_TYPE_BEVEL_RAISED = 6;
    static final int BORDER_TYPE_BEVEL_LOWERED = 7;
    static final int BORDER_TYPE_IMAGE = 8;
    static final int BORDER_TYPE_IMAGE_HORIZONTAL = 10;
    static final int BORDER_TYPE_IMAGE_VERTICAL = 11;
    static final int BORDER_TYPE_DASHED = 12;
    static final int BORDER_TYPE_DOTTED = 13;
    static final int BORDER_TYPE_DOUBLE = 14;
    static final int BORDER_TYPE_GROOVE = 15;
    static final int BORDER_TYPE_RIDGE = 16;
    static final int BORDER_TYPE_INSET = 17;
    static final int BORDER_TYPE_OUTSET = 18;
    static final int BORDER_TYPE_IMAGE_SCALED = 19;
    static final int BORDER_TYPE_IMAGE_ROUND = 20;
    static final int BORDER_TYPE_UNDERLINE = 21;
    private static final String systemResourceLocation = "/CN1Resource.res";
    private static boolean enableMediaQueries = true;
    /**
     * This variable is used by new GUI builder apps to keep track of the applications resources
     */
    private static Resources globalResources;
    /**
     * This variable is used to cache the system resources file CN1Resource.res
     */
    private static Resources systemResource;
    private static byte[] key;
    // for use by the resource editor
    private static Class classLoader = Resources.class;
    /**
     * These variables are useful for caching the last loaded resource which might
     * happen frequently in the UI builder when moving between applications screens
     */
    private static Object cachedResource;
    private static String lastLoadedName;
    private static int lastLoadedDPI;
    private static boolean runtimeMultiImages;
    private static boolean failOnMissingTruetype = true;
    short majorVersion;
    short minorVersion;
    int keyOffset;
    private String[] metaData;
    private int dpi = -1;
    /**
     * Hashtable containing the mapping between element types and their names in the
     * resource hashtable
     */
    private final HashMap<String, Byte> resourceTypes = new HashMap<String, Byte>();
    /**
     * A cache within the resource allowing us to preserve some resources in memory
     * so they can be utilized by a theme when it is loaded
     */
    private final HashMap<String, Object> resources = new HashMap<String, Object>();
    private DataInputStream input;

    // for internal use by the resource editor, creates an empty resource
    Resources() {
    }

    Resources(InputStream input, int dpi) throws IOException {
        this.dpi = dpi;
        openFile(input);
    }

    /**
     * @return the failOnMissingTruetype
     */
    public static boolean isFailOnMissingTruetype() {
        return failOnMissingTruetype;
    }

    /**
     * @param aFailOnMissingTruetype the failOnMissingTruetype to set
     */
    public static void setFailOnMissingTruetype(boolean aFailOnMissingTruetype) {
        failOnMissingTruetype = aFailOnMissingTruetype;
    }

    public static boolean isEnableMediaQueries() {
        return enableMediaQueries;
    }

    public static void setEnableMediaQueries(boolean enable) {
        enableMediaQueries = enable;
    }

    /**
     * This flag should be off and it is off by default, some special case implementations for development
     * e.g. the AWT implementation activate this flag in order to use the EncodedImage multi-image support
     * which is entirely for development only!
     *
     * @deprecated do not use this method!
     */
    public static void setRuntimeMultiImageEnabled(boolean b) {
        runtimeMultiImages = b;
    }

    static void setClassLoader(Class cls) {
        classLoader = cls;
    }

    /**
     * Sets the password to use for password protected resource files
     *
     * @param password the password or null to clear the password
     */
    public static void setPassword(String password) {
        if (password == null) {
            key = null;
            return;
        }
        try {
            key = password.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            // won't happen
            Log.e(ex);
        }
    }

    private static String[] toStringArray(ArrayList<String> v) {
        String[] s = new String[v.size()];
        int slen = v.size();
        for (int iter = 0; iter < slen; iter++) {
            s[iter] = v.get(iter);
        }
        return s;
    }

    /**
     * Opens a multi-layer resource file that supports overriding features on a specific
     * platform. Notice that the ".res" extension MUST not be given to this method!
     *
     * @param resource a local reference to a resource using the syntax of Class.getResourceAsStream(String)
     *                 however <b>the extension MUST not be included in the name!</b> E.g. to reference /x.res use /x
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources openLayered(String resource) throws IOException {
        return openLayered(resource, -1);
    }

    /**
     * Creates a resource object from the local JAR resource identifier
     *
     * @param resource a local reference to a resource using the syntax of Class.getResourceAsStream(String)
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources open(String resource) throws IOException {
        return open(resource, -1);
    }

    /**
     * Creates a resource object from the given input stream
     *
     * @param resource stream from which to read the resource
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources open(InputStream resource) throws IOException {
        return open(resource, -1);
    }

    /**
     * Opens a multi-layer resource file that supports overriding features on a specific
     * platform. Notice that the ".res" extension MUST not be given to this method!
     *
     * @param resource a local reference to a resource using the syntax of Class.getResourceAsStream(String)
     *                 however <b>the extension MUST not be included in the name!</b> E.g. to reference /x.res use /x
     * @param dpi      the dpi used for the loaded images
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources openLayered(String resource, int dpi) throws IOException {
        Resources r = open(resource + ".res", dpi);

        String[] over = Display.getInstance().getPlatformOverrides();
        int olen = over.length;
        for (int iter = 0; iter < olen; iter++) {
            InputStream i = Display.getInstance().getResourceAsStream(classLoader, resource + "_" + over[iter] + ".ovr");
            if (i != null) {
                r.override(i);
                i.close();
            }
        }

        return r;
    }

    /**
     * Creates a resource object from the local JAR resource identifier
     *
     * @param resource a local reference to a resource using the syntax of Class.getResourceAsStream(String)
     * @param dpi      the dpi used for the loaded images
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources open(String resource, int dpi) throws IOException {
        try {
            if (resource.equals(Resources.systemResourceLocation) && systemResource != null) {
                return systemResource;
            }
            if (lastLoadedName != null && lastLoadedName.equals(resource) && lastLoadedDPI == dpi) {
                Resources r = (Resources) Display.getInstance().extractHardRef(cachedResource);
                if (r != null) {
                    return r;
                }
            }
            InputStream is = Display.getInstance().getResourceAsStream(classLoader, resource);
            if (is == null) {
                throw new IOException(resource + " not found");
            }
            Resources r = new Resources(is, dpi);
            is.close();

            if (resource.equals(Resources.systemResourceLocation)) {
                systemResource = r;
                return r;
            }

            lastLoadedDPI = dpi;
            lastLoadedName = resource;
            cachedResource = Display.getInstance().createSoftWeakRef(r);
            return r;
        } catch (RuntimeException err) {
            // intercept exceptions since user code might not deal well with runtime exceptions
            Log.e(err);
            throw new IOException(err.getMessage());
        }
    }

    /**
     * Creates a resource object from the given input stream
     *
     * @param resource stream from which to read the resource
     * @param dpi      the dpi used for the loaded images
     * @return a resource object
     * @throws java.io.IOException if opening/reading the resource fails
     */
    public static Resources open(InputStream resource, int dpi) throws IOException {
        return new Resources(resource, dpi);
    }

    /**
     * Gets the system resource which can be located /CN1Images.res.
     *
     * @return a Resources object which contains the system resources
     */
    public static Resources getSystemResource() {
        try {
            return open(systemResourceLocation);
        } catch (IOException ex) {
            Log.e(ex);
        }
        return null;
    }

    /**
     * Global resources are used by new GUI builder apps to keep track of the applications resources
     *
     * @return the resource object used by default in the GUI builder forms
     */
    public static Resources getGlobalResources() {
        return globalResources;
    }

    /**
     * Global resources are used by new GUI builder apps to keep track of the applications resources
     *
     * @param res the resource object used by default in the GUI builder forms
     */
    public static void setGlobalResources(Resources res) {
        globalResources = res;
    }

    void clear() {
        majorVersion = 0;
        minorVersion = 0;
        resourceTypes.clear();
        resources.clear();
        input = null;
    }

    Timeline readTimeline(DataInputStream input) throws IOException {
        int duration = input.readInt();
        int width = input.readInt();
        int height = input.readInt();
        AnimationObject[] animations = new AnimationObject[input.readShort()];
        int alen = animations.length;
        for (int iter = 0; iter < alen; iter++) {
            String name = input.readUTF();
            int startTime = input.readInt();
            int animDuration = input.readInt();
            int x = input.readInt();
            int y = input.readInt();
            Image i = getImage(name);
            if (i == null) {
                animations[iter] = AnimationObject.createAnimationImage(name, this, x, y);
            } else {
                animations[iter] = AnimationObject.createAnimationImage(i, x, y);
            }
            animations[iter].setStartTime(startTime);
            animations[iter].setEndTime(startTime + animDuration);
            int frameDelay = input.readInt();
            if (frameDelay > -1) {
                int frameWidth = input.readInt();
                int frameHeight = input.readInt();
                animations[iter].defineFrames(frameWidth, frameHeight, frameDelay);
            }
            if (input.readBoolean()) {
                animations[iter].defineMotionX(input.readInt(), startTime, animDuration, x, input.readInt());
            }
            if (input.readBoolean()) {
                animations[iter].defineMotionY(input.readInt(), startTime, animDuration, y, input.readInt());
            }
            if (input.readBoolean()) {
                animations[iter].defineWidth(input.readInt(), startTime, animDuration, input.readInt(), input.readInt());
            }
            if (input.readBoolean()) {
                animations[iter].defineHeight(input.readInt(), startTime, animDuration, input.readInt(), input.readInt());
            }
            if (input.readBoolean()) {
                animations[iter].defineOpacity(input.readInt(), startTime, animDuration, input.readInt(), input.readInt());
            }
            if (input.readBoolean()) {
                animations[iter].defineOrientation(input.readInt(), startTime, animDuration, input.readInt(), input.readInt());
            }
        }
        Timeline tl = Timeline.createTimeline(duration, animations, new Dimension(width, height));
        return tl;
    }

    /**
     * This method is used by the Codename One Designer
     */
    void startingEntry(String id, byte magic) {
    }

    /**
     * This method allows overriding the data of a resource file with another resource file thus replacing
     * or enhancing existing content with platform specific content. E.g. default icons for the application
     * can be overriden on a specific platform
     *
     * @param input a new resource file
     * @throws IOException exception thrown from the stream
     */
    public void override(InputStream input) throws IOException {
        openFileImpl(input);
    }

    void openFile(InputStream input) throws IOException {
        clear();
        openFileImpl(input);
    }

    private void openFileImpl(InputStream input) throws IOException {
        this.input = new DataInputStream(input);
        int resourceCount = this.input.readShort();
        if (resourceCount < 0) {
            throw new IOException("Invalid resource file!");
        }
        boolean password = false;
        keyOffset = 0;
        for (int iter = 0; iter < resourceCount; iter++) {
            byte magic = this.input.readByte();
            String id = this.input.readUTF();
            if (password) {
                magic = (byte) decode(magic & 0xff);
                char[] chars = id.toCharArray();
                int clen = chars.length;
                for (int i = 0; i < clen; i++) {
                    chars[i] = (char) decode(chars[i] & 0xffff);
                }
                id = new String(chars);
            }

            startingEntry(id, magic);
            switch (magic) {
                case MAGIC_PASSWORD:
                    checkKey(id);
                    password = true;
                    continue;
                case MAGIC_HEADER:
                    readHeader();
                    continue;
                case MAGIC_THEME:
                case MAGIC_THEME_LEGACY:
                    setResource(id, MAGIC_THEME, loadTheme(id, magic == MAGIC_THEME));
                    continue;
                case MAGIC_IMAGE:
                    Image img = createImage();
                    img.setImageName(id);
                    setResource(id, magic, img);
                    continue;
                case MAGIC_FONT:
                    setResource(id, magic, loadFont(this.input, id, false));
                    continue;
                case MAGIC_DATA:
                    setResource(id, magic, createData());
                    continue;
                case MAGIC_UI:
                    setResource(id, magic, createData());
                    continue;
                case MAGIC_L10N:
                    setResource(id, magic, loadL10N());
                    continue;

                    // legacy file support to be removed
                case MAGIC_IMAGE_LEGACY:
                    setResource(id, MAGIC_IMAGE, createImage());
                    continue;
                case MAGIC_INDEXED_IMAGE_LEGACY:
                    setResource(id, MAGIC_IMAGE, createPackedImage8());
                    continue;
                case MAGIC_FONT_LEGACY:
                    setResource(id, MAGIC_FONT, loadFont(this.input, id, false));
                    continue;
                case MAGIC_INDEXED_FONT_LEGACY:
                    setResource(id, MAGIC_FONT, loadFont(this.input, id, true));
                    continue;
                default:
                    throw new IOException("Corrupt theme file unrecognized magic number: " + Integer.toHexString(magic & 0xff));
            }
        }
    }

    void checkKey(String id) {
        if (key == null) {
            throw new IllegalStateException("This is a password protected resource");
        }
        char l = (char) decode(id.charAt(0));
        char w = (char) decode(id.charAt(1));
        if (l != 'l' || w != 'w') {
            throw new IllegalStateException("Incorrect password");
        }
    }

    private int decode(int val) {
        val = key[keyOffset] ^ val;
        keyOffset++;
        if (keyOffset == key.length) {
            keyOffset = 0;
        }
        return val;
    }

    /**
     * Reads the header of the resource file
     */
    private void readHeader() throws IOException {
        // The header begins with a short denoting the size of the header section.
        // We don't currently use this value, but it must be consumed so that the
        // subsequent reads stay aligned with the original format.
        input.readShort();
        majorVersion = input.readShort();
        minorVersion = input.readShort();

        metaData = new String[input.readShort()];
        int mlen = metaData.length;
        for (int iter = 0; iter < mlen; iter++) {
            metaData[iter] = input.readUTF();
        }
    }

    /**
     * Returns the version number for this resource file.
     * This value relates to the value from the header defined by the resource file
     * specification. 0 is returned for legacy versions of the resource file format.
     *
     * @return major version number for the resource file
     */
    public int getMajorVersion() {
        return majorVersion;
    }

    /**
     * Returns the minor version number for this resource file
     * This value relates to the value from the header defined by the resource file
     * specification.
     *
     * @return minor version number for the resource file
     */
    public int getMinorVersion() {
        return minorVersion;
    }

    /**
     * Returns optional meta-data associated with the resource file
     *
     * @return optional meta-data associated with the file
     */
    public String[] getMetaData() {
        return metaData;
    }

    /**
     * Returns the names of the resources within this bundle
     *
     * @return array of names of all the resources in this bundle
     */
    public String[] getResourceNames() {
        String[] arr = new String[resourceTypes.size()];
        Iterator<String> e = resourceTypes.keySet().iterator();
        int alen = arr.length;
        for (int iter = 0; iter < alen; iter++) {
            arr[iter] = e.next();
        }
        return arr;
    }

    /**
     * Returns the names of the data resources within this bundle
     *
     * @return array of names of the data resources in this bundle
     */
    public String[] getDataResourceNames() {
        return getResourceTypeNames(MAGIC_DATA);
    }

    /**
     * Returns the names of the ui resources within this bundle
     *
     * @return array of names of the ui resources in this bundle
     */
    public String[] getUIResourceNames() {
        return getResourceTypeNames(MAGIC_UI);
    }

    /**
     * For internal use only
     */
    void setResource(String id, byte type, Object value) {
        if (value == null) {
            resources.remove(id);
            resourceTypes.remove(id);
        } else {
            resources.put(id, value);
            resourceTypes.put(id, Byte.valueOf(type));
        }
    }

    /**
     * Returns the names of the localization bundles within this bundle
     *
     * @return array of names of the localization resources in this bundle
     */
    public String[] getL10NResourceNames() {
        return getResourceTypeNames(MAGIC_L10N);
    }

    /**
     * Returns the names of the fonts within this bundle
     *
     * @return array of names of the font resources in this bundle
     */
    public String[] getFontResourceNames() {
        ArrayList<String> vec = new ArrayList<String>();
        Iterator<String> e = resourceTypes.keySet().iterator();
        while (e.hasNext()) {
            String c = e.next();
            if (isFont(c)) {
                vec.add(c);
            }
        }
        return toStringArray(vec);
    }

    /**
     * Returns the names of the images within this bundle
     *
     * @return array of names of the image resources in this bundle
     */
    public String[] getThemeResourceNames() {
        ArrayList<String> vec = new ArrayList<String>();
        Iterator<String> e = resourceTypes.keySet().iterator();
        while (e.hasNext()) {
            String c = e.next();
            if (isTheme(c)) {
                vec.add(c);
            }
        }
        return toStringArray(vec);
    }

    /**
     * Returns the names of the images within this bundle
     *
     * @return array of names of the image resources in this bundle
     */
    public String[] getImageResourceNames() {
        ArrayList<String> vec = new ArrayList<String>();
        for (Map.Entry<String, Byte> entry : resourceTypes.entrySet()) {
            String c = entry.getKey();
            if (isImage(c)) {
                vec.add(c);
            }
        }
        return toStringArray(vec);
    }

    /**
     * For internal use only
     */
    byte getResourceType(String name) {
        Byte b = resourceTypes.get(name);
        if (b == null) {
            return (byte) 0;
        }
        return b.byteValue();
    }

    private String[] getResourceTypeNames(byte b) {
        ArrayList<String> vec = new ArrayList<String>();
        for (Map.Entry<String, Byte> entry : resourceTypes.entrySet()) {
            if (entry.getValue().byteValue() == b) {
                vec.add(entry.getKey());
            }
        }
        return toStringArray(vec);
    }

    /**
     * Returns true if this is a generic data resource
     *
     * @param name the name of the resource
     * @return true if the resource is a data resource
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isL10N(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_L10N;
    }

    /**
     * Returns true if this is a theme resource
     *
     * @param name the name of the resource
     * @return true if the resource is a theme
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isTheme(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_THEME_LEGACY || b == MAGIC_THEME;
    }

    /**
     * Returns true if this is a font resource
     *
     * @param name the name of the resource
     * @return true if the resource is a font
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isFont(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_FONT || b == MAGIC_FONT_LEGACY || b == MAGIC_INDEXED_FONT_LEGACY;
    }

    /**
     * Returns true if this is an animation resource
     *
     * @param name the name of the resource
     * @return true if the resource is an animation
     * @throws NullPointerException if the resource doesn't exist
     * @deprecated animations are no longer distinguished from images in the resource file, use Image.isAnimation instead
     */
    public boolean isAnimation(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_ANIMATION_LEGACY;
    }

    /**
     * Returns true if this is a data resource
     *
     * @param name the name of the resource
     * @return true if the resource is a data resource
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isData(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_DATA;
    }

    /**
     * Returns true if this is a UI resource
     *
     * @param name the name of the resource
     * @return true if the resource is a UI resource
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isUI(String name) {
        byte b = resourceTypes.get(name).byteValue();
        return b == MAGIC_UI;
    }

    /**
     * Returns true if this is an image resource
     *
     * @param name the name of the resource
     * @return true if the resource is an image
     * @throws NullPointerException if the resource doesn't exist
     */
    public boolean isImage(String name) {
        Byte bt = resourceTypes.get(name);
        if (bt == null) {
            return false;
        }
        byte b = bt.byteValue();
        return b == MAGIC_IMAGE_LEGACY || b == MAGIC_ANIMATION_LEGACY ||
                b == MAGIC_INDEXED_IMAGE_LEGACY || b == MAGIC_IMAGE ||
                b == MAGIC_TIMELINE;
    }

    /**
     * Returns the image resource from the file
     *
     * @param id name of the image resource
     * @return cached image instance
     */
    public Image getImage(String id) {
        return (Image) resources.get(id);
    }

    /**
     * Returns the data resource from the file
     *
     * @param id name of the data resource
     * @return newly created input stream that allows reading the data of the resource
     */
    public InputStream getData(String id) {
        byte[] data = (byte[]) resources.get(id);
        if (data == null) {
            return null;
        }
        return new ByteArrayInputStream(data);
    }

    /**
     * Returns the ui resource from the file
     *
     * @param id name of the ui resource
     * @return newly created input stream that allows reading the ui of the resource
     */
    InputStream getUi(String id) {
        byte[] d = (byte[]) resources.get(id);
        if (d == null) {
            return null;
        }
        return new ByteArrayInputStream(d);
    }

    /**
     * Returns a hashmap containing localized String key/value pairs for the given locale name
     *
     * @param id     the name of the locale resource
     * @param locale name of the locale resource
     * @return Hashtable containing key value pairs for localized data
     */
    public Hashtable<String, String> getL10N(String id, String locale) {
        return (Hashtable<String, String>) ((Hashtable) resources.get(id)).get(locale);
    }

    /**
     * Returns an enumration of the locales supported by this resource id
     *
     * @param id the name of the locale resource
     * @return enumeration of strings containing bundle names
     */
    public Enumeration listL10NLocales(String id) {
        return ((Hashtable) resources.get(id)).keys();
    }

    /**
     * Returns a collection of the l10 locale names
     *
     * @param id the name of the locale resource
     * @return collection of strings containing bundle names
     */
    public Collection<String> l10NLocaleSet(String id) {
        return ((Hashtable<String, String>) resources.get(id)).keySet();
    }

    /**
     * Returns the font resource from the file
     *
     * @param id name of the font resource
     * @return cached font instance
     */
    public Font getFont(String id) {
        return (Font) resources.get(id);
    }

    /**
     * Returns the theme resource from the file
     *
     * @param id name of the theme resource
     * @return cached theme instance
     */
    public Hashtable getTheme(String id) {
        Hashtable h = (Hashtable) resources.get(id);

        // theme can be null in valid use cases such as the resource editor
        if (h != null && h.containsKey("uninitialized")) {
            Enumeration e = h.keys();
            while (e.hasMoreElements()) {
                String key = (String) e.nextElement();
                if (key.endsWith("font") || (key.endsWith("Image") && !key.endsWith("scaledImage"))) {
                    Object value = h.get(key);
                    if (value == null) {
                        throw new IllegalArgumentException("Couldn't find resource: " + key);
                    }

                    // the resource was not already loaded when we loaded the theme
                    // it must be loaded now so we can resolve the temporary name
                    if (value instanceof String) {
                        Object o;

                        // we need to trigger multi image logic  in the  resource editor
                        if (key.endsWith("Image")) {
                            o = getImage((String) value);
                        } else {
                            o = resources.get(value);
                        }
                        if (o == null) {
                            throw new IllegalArgumentException("Theme entry for " + key + " could not be found: " + value);
                        }
                        h.put(key, o);
                    }
                }
                // if this is a border we might need to do additional work for older versions
                // of Codename One and for the case of an image border where the images might not have
                // been loaded yet when the border was created
                if (key.endsWith("order")) {
                    Border b = confirmBorder(h, key);
                    h.put(key, b);
                }
            }
            h.remove("uninitialized");
        }
        return h;
    }

    private Border confirmBorder(Hashtable h, String key) {
        Object val = h.get(key);
        if (val == null) {
            return null;
        }
        if (!(val instanceof Border)) {
            String[] value = (String[]) val;
            if (value == null) {
                throw new IllegalArgumentException("Couldn't find resource: " + key);
            }

            // the resource was not already loaded when we loaded the theme
            // it must be loaded now so we can resolve the temporary name
            Border imageBorder = createImageBorder(value);
            return imageBorder;
        }
        return (Border) val;
    }

    private Border createImageBorder(String[] value) {
        if (value[0].equals("h")) {
            return Border.createHorizonalImageBorder(getImage(value[1]),
                    getImage(value[2]), getImage(value[3]));
        }
        if (value[0].equals("v")) {
            return Border.createVerticalImageBorder(getImage(value[1]),
                    getImage(value[2]), getImage(value[3]));
        }
        if (value[0].equals("s")) {
            Image[] images = new Image[value.length - 1];
            int ilen = images.length;
            for (int iter = 0; iter < ilen; iter++) {
                images[iter] = getImage(value[iter + 1]);
            }
            return Border.createImageScaledBorder(images[0], images[1], images[2],
                    images[3], images[4], images[5], images[6], images[7], images[8]);
        }
        Image[] images = new Image[value.length];
        int vlen = value.length;
        for (int iter = 0; iter < vlen; iter++) {
            images[iter] = getImage(value[iter]);
        }
        switch (images.length) {
            case 2:
                return Border.createImageBorder(images[0], images[1], null);
            case 3:
                return Border.createImageBorder(images[0], images[1], images[2]);
            case 8:
                return Border.createImageBorder(images[0], images[1], images[2],
                        images[3], images[4], images[5], images[6], images[7], null);
            default:
                return Border.createImageBorder(images[0], images[1], images[2],
                        images[3], images[4], images[5], images[6], images[7], images[8]);
        }
    }

    Object getResourceObject(String res) {
        return resources.get(res);
    }

    Image createImage() throws IOException {
        return createImage(input);
    }

    Image createImage(DataInputStream input) throws IOException {
        if (majorVersion == 0 && minorVersion == 0) {
            byte[] data = new byte[input.readInt()];
            input.readFully(data, 0, data.length);
            return EncodedImage.create(data);
        } else {
            int type = input.readByte() & 0xff;
            switch (type) {
                // PNG file
                case 0xf1:

                    // JPEG File
                case 0xf2:
                    byte[] data = new byte[input.readInt()];
                    input.readFully(data, 0, data.length);
                    if (minorVersion > 3) {
                        int width = input.readInt();
                        int height = input.readInt();
                        boolean opaque = input.readBoolean();
                        return EncodedImage.create(data, width, height, opaque);
                    }
                    return EncodedImage.create(data);

                // Indexed image
                case 0xF3:
                    return createPackedImage8();

                // SVG
                case 0xF5: {
                    int svgSize = input.readInt();
                    if (Image.isSVGSupported()) {
                        byte[] s = new byte[svgSize];
                        input.readFully(s);
                        String baseURL = input.readUTF();
                        boolean animated = input.readBoolean();
                        loadSVGRatios(input);
                        byte[] fallback = new byte[input.readInt()];
                        if (fallback.length > 0) {
                            input.readFully(fallback, 0, fallback.length);
                        }
                        return Image.createSVG(baseURL, animated, s);
                    } else {
                        svgSize -= input.skip(svgSize);
                        while (svgSize > 0) {
                            svgSize -= input.skip(svgSize);
                        }
                        // read the base url, the animated property and screen ratios to skip them as well...
                        input.readUTF();
                        input.readBoolean();
                        input.readFloat();
                        input.readFloat();

                        byte[] fallback = new byte[input.readInt()];
                        input.readFully(fallback, 0, fallback.length);
                        return EncodedImage.create(fallback);
                    }
                }

                // SVG with multi-image
                case 0xf7: {
                    int svgSize = input.readInt();
                    if (Image.isSVGSupported()) {
                        byte[] s = new byte[svgSize];
                        input.readFully(s);
                        input.readUTF();
                        boolean animated = input.readBoolean();
                        Image img = readMultiImage(input, true);
                        Image svg = createSVG(animated, s);
                        if (svg.getSVGDocument() == null) {
                            return img;
                        }
                        return svg;
                    } else {
                        svgSize -= input.skip(svgSize);
                        while (svgSize > 0) {
                            svgSize -= input.skip(svgSize);
                        }
                        String baseURL = input.readUTF();

                        // read the animated property to skip it as well...
                        input.readBoolean();
                        return readMultiImage(input);
                    }
                }

                // mutli image
                case 0xF6:
                    return readMultiImage(input);

                case 0xEF:
                    Timeline tl = readTimeline(input);
                    return tl;

                // Fail this is the wrong data type
                default:
                    throw new IOException("Illegal type while creating image: " + Integer.toHexString(type));
            }
        }
    }

    com.codename1.ui.Image createSVG(boolean animated, byte[] data) throws IOException {
        return Image.createSVG(null, animated, data);
    }

    private Image readMultiImage(DataInputStream input) throws IOException {
        return readMultiImage(input, false);
    }

    Image readMultiImage(DataInputStream input, boolean skipAll) throws IOException {
        EncodedImage resultImage = null;
        if (dpi == -1) {
            dpi = Display.getInstance().getDeviceDensity();
        }
        int dpiCount = input.readInt();
        int bestFitOffset = 0;
        int bestFitDPI = 0;
        int[] lengths = new int[dpiCount];
        int[] dpis = new int[dpiCount];
        boolean found = false;
        for (int iter = 0; iter < dpiCount; iter++) {
            int currentDPI = input.readInt();
            lengths[iter] = input.readInt();
            dpis[iter] = currentDPI;
            if (bestFitDPI != dpi && dpi >= currentDPI && currentDPI >= bestFitDPI) {
                bestFitDPI = currentDPI;
                bestFitOffset = iter;
                found = true;
            }
        }
        if (!found) {
            // special case for low DPI devices when running with high resolution resources
            // we want to pick the loweset resolution possible
            bestFitDPI = dpis[0];
            bestFitOffset = 0;
            for (int iter = 1; iter < dpiCount; iter++) {
                if (dpis[iter] < bestFitDPI) {
                    bestFitDPI = dpis[iter];
                    bestFitOffset = iter;
                }
            }
        }

        if (runtimeMultiImages && !skipAll) {
            byte[][] data = new byte[dpiCount][];
            for (int iter = 0; iter < lengths.length; iter++) {
                int size = lengths[iter];
                data[iter] = new byte[size];
                input.readFully(data[iter], 0, size);
            }
            return EncodedImage.createMulti(dpis, data);
        } else {
            for (int iter = 0; iter < lengths.length; iter++) {
                int size = lengths[iter];
                if (!skipAll && bestFitOffset == iter) {
                    byte[] multiImageData = new byte[size];
                    input.readFully(multiImageData, 0, size);
                    resultImage = EncodedImage.create(multiImageData);
                } else {
                    while (size > 0) {
                        size -= input.skip(size);
                    }
                }
            }
        }

        return resultImage;
    }

    void loadSVGRatios(DataInputStream input) throws IOException {
        input.readFloat();
        input.readFloat();
    }

    private byte[] createData() throws IOException {
        byte[] data = new byte[input.readInt()];
        input.readFully(data);
        return data;
    }

    Font loadFont(DataInputStream input, String id, boolean packed) throws IOException {
        if (majorVersion == 0 && minorVersion == 0) {
            Image bitmap;
            if (packed) {
                bitmap = createPackedImage8();
            } else {
                bitmap = createImage(input);
            }
            int charCount = input.readShort();
            int[] cutOffsets = new int[charCount];
            int[] charWidth = new int[charCount];
            for (int iter = 0; iter < charCount; iter++) {
                cutOffsets[iter] = input.readShort();
                charWidth[iter] = input.readByte();
            }
            String charset = input.readUTF();
            Font old = Font.getBitmapFont(id);
            if (old != null) {
                return old;
            }
            return Font.createBitmapFont(id, bitmap, cutOffsets, charWidth, charset);
        }

        // read a system font fallback
        int fallback = input.readByte() & 0xff;

        // do we have an emedded truetype font? Do we support embedded fonts?
        boolean trueTypeIncluded = input.readBoolean();
        Font font = null;
        /*if(trueTypeIncluded) {
            int size = input.readInt();
            if(Font.isTrueTypeFileSupported()) {
                font = Font.createTrueTypeFont(input);
            } else {
                while(size > 0) {
                    size -= input.skip(size);
                }
            }
        }*/
        boolean lookupIncluded = input.readBoolean();
        if (lookupIncluded) {
            String lookup = input.readUTF();
            if (font == null && Font.isCreationByStringSupported()) {
                font = Font.create(lookup);
            }
        }
        boolean bitmapIncluded = input.readBoolean();
        if (bitmapIncluded) {
            font = loadBitmapFont(input, id, font);
        }
        if (font != null) {
            return font;
        }
        return Font.createSystemFont(fallback & (Font.FACE_MONOSPACE | Font.FACE_PROPORTIONAL | Font.FACE_SYSTEM),
                fallback & (Font.STYLE_BOLD | Font.STYLE_ITALIC | Font.STYLE_PLAIN | Font.STYLE_UNDERLINED),
                fallback & (Font.SIZE_LARGE | Font.SIZE_MEDIUM | Font.SIZE_SMALL));
    }

    void readRenderingHint(DataInputStream i) throws IOException {
        i.readByte();
    }

    Font loadBitmapFont(DataInputStream input, String id, com.codename1.ui.Font font) throws IOException {
        Image bitmap = createImage(input);
        int charCount = input.readShort();
        int[] cutOffsets = new int[charCount];
        int[] charWidth = new int[charCount];
        for (int iter = 0; iter < charCount; iter++) {
            cutOffsets[iter] = input.readShort();
        }
        for (int iter = 0; iter < charCount; iter++) {
            charWidth[iter] = input.readByte();
        }
        String charset = input.readUTF();
        readRenderingHint(input);
        if (font == null) {
            if (Font.isBitmapFontEnabled()) {
                Font old = Font.getBitmapFont(id);
                if (old != null) {
                    // Returning bitmap font from cache, to prevent collision with an
                    // old resource file use Font.clearBitmapCache()
                    return old;
                }
                return Font.createBitmapFont(id, bitmap, cutOffsets, charWidth, charset);
            }
        }
        return font;
    }

    Font createTrueTypeFont(Font f, String fontName, String fileName, float fontSize, int sizeSetting) {
        switch (sizeSetting) {
            case 0: // small
                fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL).getHeight();
                break;
            case 1: // medium
                fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM).getHeight();
                break;
            case 2: // large
                fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE).getHeight();
                break;
            case 3: // millimetres
                fontSize = Display.getInstance().convertToPixels((int) (fontSize * 10), true) / 10.0f;
                break;

            case 4: // pixels
                break;
            case 5: // rem
                fontSize = Font.getDefaultFont().getHeight() * fontSize;
                break;
            case 6: // vw
                fontSize = CN.getDisplayWidth() * fontSize / 100f;
                break;
            case 7: // vh
                fontSize = CN.getDisplayHeight() * fontSize / 100f;
                break;
            case 8: // vmin
                fontSize = Math.min(CN.getDisplayWidth(), CN.getDisplayHeight()) * fontSize / 100f;
                break;
            case 9: // vmax
                fontSize = Math.max(CN.getDisplayWidth(), CN.getDisplayHeight()) * fontSize / 100f;
                break;
        }
        if (!failOnMissingTruetype) {
            try {
                Font ttf = Font.createTrueTypeFont(fontName, fileName);
                if (ttf != null) {
                    return ttf.derive(fontSize, f.getStyle());
                }
                return f;
            } catch (Exception ex) {
                return f;
            }
        }
        return Font.createTrueTypeFont(fontName, fileName).derive(fontSize, f.getStyle());
    }

    Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
        Hashtable theme = new Hashtable();
        String densityStr = Display.getInstance().getDensityStr();
        String platformName = Display.getInstance().getPlatformName();
        if ("HTML5".equals(platformName)) {
            platformName = Display.getInstance().getProperty("HTML5.platformName", "mac");
        }
        String deviceType = Display.getInstance().isDesktop() ? "desktop" : Display.getInstance().isTablet() ? "tablet" : "phone";
        String platformPrefix = "platform-" + platformName + "-";
        String densityPrefix = "density-" + densityStr + "-";
        String devicePrefix = "device-" + deviceType + "-";
        theme.put("name", id);

        // marks the theme as uninitialized so we can finish "wiring" cached resources
        theme.put("uninitialized", Boolean.TRUE);
        int size = input.readShort();
        List<String> fontKeys = new ArrayList<String>();
        Map<String, Float> fontScaleRules = null;
        class MediaRule {
            int matchCount;
            int bestMatchScore;
            String rawKey;
            String translatedKey;


        }
        Map<String, MediaRule> mediaRules = new HashMap<String, MediaRule>();
        int defaultFontSizeSetPriority = 0;
        for (int iter = 0; iter < size; iter++) {
            String key = input.readUTF();
            if (key.startsWith("@")) {
                theme.put(key, input.readUTF());
                if (enableMediaQueries) {
                    if (key.endsWith("-font-scale")) {
                        if (fontScaleRules == null) {
                            fontScaleRules = new LinkedHashMap<String, Float>();
                        }
                        fontScaleRules.put(key, Float.parseFloat((String) theme.get(key)));
                    }
                }
                if (key.equals("@defaultFontSizeInt") && defaultFontSizeSetPriority < 1) {
                    int themeMedianFontSize = Integer.parseInt((String) theme.get(key));
                    if (themeMedianFontSize > 0) {
                        double adjustedFontSize = themeMedianFontSize * CN.convertToPixels(1f) * 25.4 / (CN.isDesktop() ? 96f : 160f);
                        Font.setDefaultFont(Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR, (float) adjustedFontSize / CN.convertToPixels(1f)));
                        defaultFontSizeSetPriority = 1;
                    }
                }
                if (CN.isTablet() && key.equals("@defaultTabletFontSizeInt") && defaultFontSizeSetPriority < 2) {
                    int themeMedianFontSize = Integer.parseInt((String) theme.get(key));
                    if (themeMedianFontSize > 0) {
                        double adjustedFontSize = themeMedianFontSize * CN.convertToPixels(1f) * 25.4 / (CN.isDesktop() ? 96f : 160f);
                        Font.setDefaultFont(Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR, (float) adjustedFontSize / CN.convertToPixels(1f)));
                        defaultFontSizeSetPriority = 2;
                    }
                }
                if (CN.isDesktop() && key.equals("@defaultDesktopFontSizeInt") && defaultFontSizeSetPriority < 3) {
                    int themeMedianFontSize = Integer.parseInt((String) theme.get(key));
                    if (themeMedianFontSize > 0) {
                        double adjustedFontSize = themeMedianFontSize * CN.convertToPixels(1f) * 25.4 / (CN.isDesktop() ? 96f : 160f);
                        Font.setDefaultFont(Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR, (float) adjustedFontSize / CN.convertToPixels(1f)));
                        defaultFontSizeSetPriority = 3;
                    }
                }
                continue;
            }
            if (enableMediaQueries) {
                String subkey = key;
                boolean mediaMatch = true;
                int matchCount = 0;
                int bestMatchScore = 0;
                if (subkey.startsWith("device-")) {
                    if (!subkey.startsWith(devicePrefix)) {
                        mediaMatch = false;
                    } else {
                        subkey = subkey.substring(devicePrefix.length());
                        matchCount++;
                        bestMatchScore = Math.max(50, bestMatchScore);
                    }

                }
                if (mediaMatch && subkey.startsWith("platform-")) {
                    if (!subkey.startsWith(platformPrefix)) {
                        mediaMatch = false;
                    } else {
                        subkey = subkey.substring(platformPrefix.length());
                        matchCount++;
                        bestMatchScore = Math.max(100, bestMatchScore);
                    }
                }
                if (mediaMatch && subkey.startsWith("density-")) {
                    if (!subkey.startsWith(densityPrefix)) {
                        mediaMatch = false;
                    } else {
                        subkey = subkey.substring(densityPrefix.length());
                        matchCount++;
                        bestMatchScore = Math.max(10, bestMatchScore);
                    }
                }

                if (mediaMatch) {
                    if (mediaRules == null) {
                        mediaRules = new HashMap<String, MediaRule>();
                    }
                    MediaRule rule = mediaRules.get(subkey);
                    boolean replace = rule == null;
                    if (!replace && rule.matchCount <= matchCount) {
                        replace = true;
                    }
                    if (!replace && rule.bestMatchScore <= bestMatchScore) {
                        replace = true;
                    }
                    if (replace) {
                        if (rule == null) {
                            rule = new MediaRule();
                            mediaRules.put(subkey, rule);
                        }
                        rule.bestMatchScore = bestMatchScore;
                        rule.matchCount = matchCount;
                        rule.rawKey = key;
                        rule.translatedKey = subkey;
                    }

                }

            }
            // if this is a simple numeric value
            if (key.endsWith("Color")) {

                theme.put(key, Integer.toHexString(input.readInt()));
                continue;
            }

            if (key.endsWith("align") || key.endsWith("textDecoration")) {
                theme.put(key, Integer.valueOf(input.readShort()));
                continue;
            }

            // if this is a short numeric value for transparency
            if (key.endsWith("ransparency")) {
                theme.put(key, "" + (input.readByte() & 0xff));
                continue;
            }

            if (key.endsWith("fgAlpha")) {
                theme.put(key, (input.readInt() & 0xff));
                continue;
            }
            if (key.endsWith("opacity")) {
                theme.put(key, "" + (input.readInt() & 0xff));
                continue;
            }

            if (key.endsWith("elevation")) {
                theme.put(key, (input.readInt() & 0xff));
                continue;
            }

            if (key.endsWith("iconGap")) {
                theme.put(key, input.readFloat());
                continue;
            }

            if (key.endsWith("iconGapUnit")) {
                theme.put(key, input.readByte());
                continue;
            }

            if (key.endsWith("surface")) {
                theme.put(key, (input.readBoolean()));
                continue;
            }

            // if this is a padding or margin then we will have the 4 values as bytes
            if (key.endsWith("adding") || key.endsWith("argin")) {
                if (minorVersion > 7) {
                    float p1 = input.readFloat();
                    float p2 = input.readFloat();
                    float p3 = input.readFloat();
                    float p4 = input.readFloat();
                    theme.put(key, "" + p1 + "," + p2 + "," + p3 + "," + p4);
                } else {
                    int p1 = input.readByte() & 0xff;
                    int p2 = input.readByte() & 0xff;
                    int p3 = input.readByte() & 0xff;
                    int p4 = input.readByte() & 0xff;
                    theme.put(key, "" + p1 + "," + p2 + "," + p3 + "," + p4);
                }
                continue;
            }

            // padding and or margin type
            if (key.endsWith("Unit")) {
                byte p1 = input.readByte();
                byte p2 = input.readByte();
                byte p3 = input.readByte();
                byte p4 = input.readByte();
                theme.put(key, new byte[]{p1, p2, p3, p4});
                continue;
            }

            // border
            if (key.endsWith("order")) {
                int borderType = input.readShort() & 0xffff;
                Object b = createBorder(input, borderType);
                theme.put(key, b);
                continue;
            }

            // if this is a font
            if (key.endsWith("ont")) {
                Font f;

                // is this a new font?
                if (input.readBoolean()) {
                    String fontId = input.readUTF();
                    f = (Font) resources.get(fontId);

                    // if the font is not yet loaded
                    if (f == null) {
                        theme.put(key, fontId);
                        continue;
                    }
                } else {
                    f = Font.createSystemFont(input.readByte(), input.readByte(), input.readByte());
                    if (minorVersion > 4) {
                        boolean hasTTF = input.readBoolean();
                        if (hasTTF) {
                            String fileName = input.readUTF();
                            String fontName = input.readUTF();
                            int sizeSetting = input.readInt();
                            float fontSize = input.readFloat();
                            if (Font.isTrueTypeFileSupported()) {
                                fontKeys.add(key);
                                f = createTrueTypeFont(f, fontName, fileName, fontSize, sizeSetting);
                            }
                        }
                    }
                }
                theme.put(key, f);
                continue;
            }

            // the background property
            if (key.endsWith("ackground")) {
                int type = input.readByte() & 0xff;
                int pos = key.indexOf('.');
                if (pos > -1) {
                    key = key.substring(0, pos);
                } else {
                    key = "";
                }
                theme.put(key + Style.BACKGROUND_TYPE, Byte.valueOf((byte) type));

                switch (type) {
                    // Scaled Image
                    case 0xF1:
                        // Tiled Both Image
                    case MAGIC_INDEXED_IMAGE_LEGACY:
                        // the image name coupled with the type
                        theme.put(key + Style.BG_IMAGE, input.readUTF());
                        break;

                    // Aligned Image
                    case 0xf5:
                        // Tiled Vertically Image
                    case 0xF2:
                        // Tiled Horizontally Image
                    case 0xF3:
                        // the image name coupled with the type and with alignment information
                        String imageName = input.readUTF();
                        theme.put(key + Style.BG_IMAGE, imageName);
                        byte align = input.readByte();
                        theme.put(key + Style.BACKGROUND_ALIGNMENT, Byte.valueOf(align));
                        break;

                    // Horizontal Linear Gradient
                    case 0xF6:
                        // Vertical Linear Gradient
                    case 0xF7:
                        Float c = new Float(0.5f);
                        theme.put(key + Style.BACKGROUND_GRADIENT, new Object[]{Integer.valueOf(input.readInt()), Integer.valueOf(input.readInt()), c, c, new Float(1)});
                        break;

                    // Radial Gradient
                    case 0xF8:
                        int c1 = input.readInt();
                        int c2 = input.readInt();
                        float f1 = input.readFloat();
                        float f2 = input.readFloat();
                        float radialSize = 1;
                        if (minorVersion > 1) {
                            radialSize = input.readFloat();
                        }
                        theme.put(key + Style.BACKGROUND_GRADIENT, new Object[]{Integer.valueOf(c1),
                                Integer.valueOf(c2),
                                new Float(f1),
                                new Float(f2),
                                new Float(radialSize)});
                        break;
                }
                continue;
            }

            // if this is a background image bgImage
            if (key.endsWith("derive")) {
                theme.put(key, input.readUTF());
                continue;
            }

            // if this is a background image bgImage
            if (key.endsWith("bgImage")) {
                String imageId = input.readUTF();
                Image i = getImage(imageId);

                // if the font is not yet loaded
                if (i == null) {
                    theme.put(key, imageId);
                    continue;
                }
                theme.put(key, i);
                continue;
            }

            if (key.endsWith("scaledImage")) {
                if (input.readBoolean()) {
                    theme.put(key, "true");
                } else {
                    theme.put(key, "false");
                }
                continue;
            }

            if (key.endsWith(Style.BACKGROUND_TYPE) || key.endsWith(Style.BACKGROUND_ALIGNMENT)) {
                theme.put(key, Byte.valueOf(input.readByte()));
                continue;
            }

            if (key.endsWith(Style.BACKGROUND_GRADIENT)) {
                if (minorVersion < 2) {
                    theme.put(key, new Object[]{
                            Integer.valueOf(input.readInt()),
                            Integer.valueOf(input.readInt()),
                            new Float(input.readFloat()),
                            new Float(input.readFloat())
                    });
                } else {
                    theme.put(key, new Object[]{
                            Integer.valueOf(input.readInt()),
                            Integer.valueOf(input.readInt()),
                            new Float(input.readFloat()),
                            new Float(input.readFloat()),
                            new Float(input.readFloat())
                    });
                }
                continue;
            }

            // thow an exception no idea what this is
            throw new IOException("Error while trying to read theme property: " + key);
        }
        if (enableMediaQueries) {
            if (mediaRules != null && !mediaRules.isEmpty()) {
                for (MediaRule rule : mediaRules.values()) {
                    theme.put(rule.translatedKey, theme.get(rule.rawKey));
                }
            }

            if (fontScaleRules != null && !fontKeys.isEmpty()) {
                float scale = 1f;

                for (Map.Entry<String, Float> rule : fontScaleRules.entrySet()) {
                    String key = rule.getKey();
                    String skey = key.substring(1);
                    if (skey.startsWith("device-")) {
                        if (skey.startsWith(devicePrefix)) {
                            skey = skey.substring(devicePrefix.length());
                        } else {
                            continue;
                        }
                    }
                    if (skey.startsWith("platform-")) {
                        if (skey.startsWith(platformPrefix)) {
                            skey = skey.substring(platformPrefix.length());
                        } else {
                            continue;
                        }

                    }

                    if (skey.startsWith("density-")) {
                        if (skey.startsWith(densityPrefix)) {
                            skey = skey.substring(densityPrefix.length());
                        } else {
                            continue;
                        }
                    }

                    if ("font-scale".equals(skey)) {
                        scale *= rule.getValue();
                    }


                }
                if (Math.abs(scale - 1f) > 0.01) {
                    for (String fontKey : fontKeys) {
                        Font f = (Font) theme.get(fontKey);
                        if (f != null && f.isTTFNativeFont()) {
                            try {
                                f = f.derive(f.getPixelSize() * scale, f.getStyle());
                                theme.put(fontKey, f);
                            } catch (Exception ex) {
                                Log.p("Failed to derive font " + f + " while loading font key " + fontKey + " from resource file. " + ex.getMessage());
                                Log.e(ex);
                            }
                        }

                    }
                }
            }
        }
        return theme;
    }

    private Object createBorder(DataInputStream input, int type) throws IOException {
        switch (type) {
            // empty border
            case 0xff01:
                return Border.getEmpty();

            // Line border
            case 0xff02:
                // use theme colors?
                if (minorVersion > 8) {
                    if (input.readBoolean()) {
                        if (input.readBoolean()) {
                            return Border.createLineBorder(input.readFloat());
                        }
                        return Border.createLineBorder((int) input.readFloat());
                    } else {
                        if (input.readBoolean()) {
                            return Border.createLineBorder(input.readFloat(), input.readInt());
                        }
                        return Border.createLineBorder((int) input.readFloat(), input.readInt());
                    }
                }
                if (input.readBoolean()) {
                    return Border.createLineBorder(input.readByte());
                } else {
                    return Border.createLineBorder(input.readByte(), input.readInt());
                }

                // Underline border
            case 0xff14:
                // use theme colors?
                if (input.readBoolean()) {
                    if (input.readBoolean()) {
                        return Border.createUndelineBorder(input.readFloat());
                    }
                    return Border.createUndelineBorder((int) input.readFloat());
                } else {
                    if (input.readBoolean()) {
                        return Border.createUnderlineBorder(input.readFloat(), input.readInt());
                    }
                    return Border.createUnderlineBorder((int) input.readFloat(), input.readInt());
                }

                // Rounded border
            case 0xff03:
                // use theme colors?
                if (input.readBoolean()) {
                    return Border.createRoundBorder(input.readByte(), input.readByte());
                } else {
                    return Border.createRoundBorder(input.readByte(), input.readByte(), input.readInt());
                }

                // Etched Lowered border
            case 0xff04:
                // use theme colors?
                if (input.readBoolean()) {
                    return Border.createEtchedLowered();
                } else {
                    return Border.createEtchedLowered(input.readInt(), input.readInt());
                }

                // Etched raised border
            case 0xff05:
                // use theme colors?
                if (input.readBoolean()) {
                    return Border.createEtchedRaised();
                } else {
                    return Border.createEtchedRaised(input.readInt(), input.readInt());
                }

                // Bevel raised
            case 0xff07:
                // use theme colors?
                if (input.readBoolean()) {
                    return Border.createBevelRaised();
                } else {
                    return Border.createBevelRaised(input.readInt(), input.readInt(), input.readInt(), input.readInt());
                }

                // Bevel lowered
            case 0xff06:
                // use theme colors?
                if (input.readBoolean()) {
                    return Border.createBevelLowered();
                } else {
                    return Border.createBevelLowered(input.readInt(), input.readInt(), input.readInt(), input.readInt());
                }

                // Image border
            case 0xff08:
                Object[] imageBorder = readImageBorder(input);
                return imageBorder;

            // horizontal Image border
            case 0xff09:
                return readImageBorder(input, "h");

            // vertical Image border
            case 0xff10:
                return readImageBorder(input, "v");

            // scaled Image border
            case 0xff11:
                return readScaledImageBorder(input);

            // round border
            case 0xff12:
                return RoundBorder.create().
                        rectangle(input.readBoolean()).
                        color(input.readInt()).
                        opacity(input.readInt()).
                        stroke(input.readFloat(), input.readBoolean()).
                        strokeColor(input.readInt()).
                        strokeOpacity(input.readInt()).
                        shadowBlur(input.readFloat()).
                        shadowOpacity(input.readInt()).
                        shadowSpread(input.readInt(), input.readBoolean()).
                        shadowX(input.readFloat()).
                        shadowY(input.readFloat());

            // round rect border
            case 0xff13:
                RoundRectBorder out = RoundRectBorder.create().
                        stroke(input.readFloat(), input.readBoolean()).
                        strokeColor(input.readInt()).
                        strokeOpacity(input.readInt()).
                        shadowBlur(input.readFloat()).
                        shadowOpacity(input.readInt()).
                        shadowSpread(input.readFloat()).
                        shadowX(input.readFloat()).
                        shadowY(input.readFloat()).
                        cornerRadius(input.readFloat()).
                        bezierCorners(input.readBoolean());
                if (input.readBoolean()) {
                    out.topOnlyMode(true);
                }
                if (input.readBoolean()) {
                    out.bottomOnlyMode(true);
                }
                return out;

            //topOnlyMode(input.readBoolean()).
            //bottomOnlyMode(input.readBoolean());
            // round rect border with top-left, top-right, bottom-right, bottom-left corners
            // specified independently
            case 0xff15:
                return RoundRectBorder.create().
                        stroke(input.readFloat(), input.readBoolean()).
                        strokeColor(input.readInt()).
                        strokeOpacity(input.readInt()).
                        shadowBlur(input.readFloat()).
                        shadowOpacity(input.readInt()).
                        shadowSpread(input.readFloat()).
                        shadowX(input.readFloat()).
                        shadowY(input.readFloat()).
                        cornerRadius(input.readFloat()).
                        bezierCorners(input.readBoolean()).
                        topLeftMode(input.readBoolean()).
                        topRightMode(input.readBoolean()).
                        bottomRightMode(input.readBoolean()).
                        bottomLeftMode(input.readBoolean());

            case 0xff16:
                // CSS border
                String cssStyles = input.readUTF();
                try {
                    return new CSSBorder(this, cssStyles);
                } catch (Throwable t) {
                    Log.p("Failed to load CSS border: " + cssStyles);
                    Log.e(t);
                    return Border.createEmpty();
                }

        }
        return null;
    }

    private String[] readImageBorder(DataInputStream input, String orientation) throws IOException {
        String[] imageBorder = new String[4];
        imageBorder[0] = orientation;
        for (int iter = 1; iter < 4; iter++) {
            imageBorder[iter] = input.readUTF();
        }
        return imageBorder;
    }

    private String[] readScaledImageBorder(DataInputStream input) throws IOException {
        String[] a = readImageBorder(input);
        String[] b = new String[a.length + 1];
        System.arraycopy(a, 0, b, 1, a.length);
        b[0] = "s";
        return b;
    }

    private String[] readImageBorder(DataInputStream input) throws IOException {
        // Read number of images can be 2, 3, 8 or 9
        int size = input.readByte();
        String[] imageBorder = new String[size];

        for (int iter = 0; iter < size; iter++) {
            imageBorder[iter] = input.readUTF();
        }
        return imageBorder;
    }

    private Hashtable loadL10N() throws IOException {
        Hashtable<String, Hashtable<String, String>> l10n = new Hashtable<String, Hashtable<String, String>>();

        int keys = input.readShort();
        int languages = input.readShort();
        String[] keyArray = new String[keys];
        for (int iter = 0; iter < keys; iter++) {
            String key = input.readUTF();
            keyArray[iter] = key;
        }
        for (int iter = 0; iter < languages; iter++) {
            Hashtable currentLanguage = new Hashtable();
            String lang = input.readUTF();
            l10n.put(lang, currentLanguage);
            for (int valueIter = 0; valueIter < keys; valueIter++) {
                currentLanguage.put(keyArray[valueIter], input.readUTF());
            }
        }
        return l10n;
    }

    /**
     * Creates a packed image from the input stream for an 8 bit packed image
     */
    private Image createPackedImage8() throws IOException {
        // read the length of the palette;
        int size = input.readByte() & 0xff;

        // 0 means the last bit overflowed, there is no sense for 0 sized palette
        if (size == 0) {
            size = 256;
        }
        int[] palette = new int[size];
        int plen = palette.length;
        for (int iter = 0; iter < plen; iter++) {
            palette[iter] = input.readInt();
        }
        int width = input.readShort();
        int height = input.readShort();
        byte[] data = new byte[width * height];
        input.readFully(data, 0, data.length);
        return Image.createIndexed(width, height, palette, data);
    }
}
